"""
Utility classes that parse the input file and build the n-ary tree
used by Drawer
"""

import re
import tree
import sys

__all__ = [ 'Parser', 'ParseException' ]

class ParseException(Exception):
    """
    This exception is raised in case of some parsing error
    """

    def __init__(self, line, value):
        self.error = value
        self.line = line

    def __str__(self):
        return repr("Parser error at line %d: %s" % (self.line, self.error))


class Parser():
    """
    Parses the input file and builds the n-ary tree used by Drawer
    """

    valid_line_heading  = re.compile(r'^:')
    valid_line_tree     = re.compile(r'^[*+-]{1,} [A-Z0-9_]{2,}', re.IGNORECASE)
    valid_line_comment  = re.compile(r'^#')

    # TODO: find a less resource-expensive and elegant way to check these
    # (better if it doesn't involves loops as these can be called a LOT of times)
    indent_namespace    = re.compile(r'^[-]+$')
    indent_class        = re.compile(r'^[\+]+$')
    indent_func         = re.compile(r'^[\*]+$')

    def __init__(self, filename):
        self.filename = filename
        self.parse_tree = []
        self.headers = []


    def readfile(self):
        """
        A generator that reads the input file and for every iteration
        returns a tuple with the stripped line and the line number.
        """
        line_num = 0
        for line in file(self.filename):
            line_num = line_num + 1
            yield line.strip(), line_num

    def parse(self):
        """
        Builds the n-ary tree that will be used later by the image
        writer (additionally set the correct headings).
        """

        # keep this in sync with valid_line_*
        valid_line = re.compile(
                '^:|'                   # heading
                '^[*+-]{1,} '           # tree stuff
                '[A-Z0-9_]{2,}|'        # valid python name
                '^#',                   # comment
                re.IGNORECASE)

        STATE_HEADER, STATE_TREE, STATE_INVALID = 0, 1, 2

        current = STATE_HEADER
        try:
            for line, line_num in self.readfile():
                if line == "":
                    continue

                if current == STATE_HEADER:
                    # beginning of the document
                    if self.valid_line_heading.match(line):
                        self.add_header(line)
                    elif self.valid_line_tree.match(line):
                        self.add_to_tree(line)
                        current = STATE_TREE
                    elif self.valid_line_comment.match(line):
                        continue
                    else:
                        current = STATE_INVALID
                        raise ParseException(line_num, "unexepected char")

                elif current == STATE_TREE:
                    # we're now parsing the tree
                    if self.valid_line_tree.match(line):
                        self.add_to_tree(line)
                    elif self.valid_line_comment.match(line):
                        continue
                    else:
                        current = STATE_INVALID
                        raise ParseException(line_num, "unexepected char")

                else:
                    # we're in a unknown state, this should never happen
                    # but just in case handle it properly
                    current = STATE_INVALID
                    raise ParseException(line_num, "state machine went in an unexpected state")

        except IOError:
            raise IOError, "Can't open " + self.filename
            return False

        return self.build_tree()

    def add_to_tree(self, line):
        """
        Add a line to tree. Note that this doesn't bulds the real tree, this just
        insert the line in a list that will be used by ``build_tree()`` later to
        build the real tree
        """
        self.parse_tree.append(line)

    def add_header(self, line):
        """
        Add a header line to the document
        """

        # an header begins with ':', so let strip the first char and
        # remove leading and trailing spaces
        self.headers.append(line[1:].strip())

    def build_tree(self, line_num=0, parentTreePath=None):
        """
        Build the n-ary tree.
        """

        try:
            indent, function = self.split_tree_line(line_num)
        except IndexError:
            raise ParseException(line_num, "invalid line")
        level = len(indent)

        node_type = self.type_of_indent(indent)
        if node_type not in [tree.TYPE_NAMESPACE, tree.TYPE_CLASS, tree.TYPE_FUNC]:
            raise IndentationError, "Invalid indentation option (line was: %(line)s)" % \
                    { 'line': self.parse_tree[line_num] }

        if not parentTreePath:
            parentTreePath = function
        else:
            parentTreePath = parentTreePath + "." + function

        node = tree.Node(node_type, function, parentTreePath)

        i = line_num + 1
        while i < len(self.parse_tree):
            try:
                new_level, new_function = self.split_tree_line(i)
            except IndexError:
                raise ParseException(line_num, "invalid line")

            if len(new_level) == level + 1:
                new_node = self.build_tree(i, parentTreePath)
                node.add(new_node)
            elif len(new_level) <= level:
                break
            i = i + 1

        return node

    def type_of_indent(self, indent):
        """
        Returns the type of node
        """
        if self.indent_namespace.match(indent):
            return tree.TYPE_NAMESPACE
        elif self.indent_class.match(indent):
            return tree.TYPE_CLASS
        elif self.indent_func.match(indent):
            return tree.TYPE_FUNC
        else:
            return -1

    def split_tree_line(self, line_num):
        """
        Returns a tuple containing the indent in the first item and
        the line in the second
        """
        splitted = self.parse_tree[line_num].split(' ')
        return splitted[0], ' '.join(splitted[1:])


if __name__ == '__main__':
    import sys
    import drawer
    drawer.Drawer(Parser(sys.argv[1]).parse(), "output.svg").draw()

